using System;

namespace Miado.Threading
{
    /// <summary>
    /// This class provides a mechanism for caching data for 
    /// a given time.  You provide it with a delegate that will 
    /// be used to re-populate itself along with a time-to-live 
    /// in milliseconds.
    /// </summary>
    /// <typeparam name="T">the type of object to be cached</typeparam>
    public class CachedData<T>
    {
        #region Members

        private const int TIMEOUT = 60 * 1000; // 60 seconds

        #endregion

        #region Constructors

        /// <summary>
        /// No-arg constructor for sub-classes
        /// </summary>
        protected CachedData()
        {
            LastUpdatedAt = DateTime.MinValue;
            LockHandle = new object();
        }

        /// <summary>
        /// Constructor - the time-to-live will be defaulted to one hour
        /// </summary>
        /// <param name="populatingDelegate">a delegate that will be used to 
        /// populate the cache</param>
        public CachedData(Func<T> populatingDelegate) :
            this(populatingDelegate, 60 * 60 * 1000) { }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="populatingDelegate">a delegate that will be used to 
        /// populate the cache</param>
        /// <param name="timeToLive">the time-to-live in milliseconds</param>
        public CachedData(Func<T> populatingDelegate, int timeToLive) : this()
        {
            this.CachePopulater = populatingDelegate;
            this.TimeToLive = timeToLive;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the time-to-live of the cache (in milliseconds)
        /// </summary>
        public int TimeToLive
        {
            get; 
            set;
        }

        /// <summary>
        /// Gets or sets the no-arg method used to populate 
        /// the data in the cache
        /// </summary>
        /// <returns>the delegate method used to populate the cache</returns>      
        private Func<T> CachePopulater
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the data stored in the cache.
        /// </summary>
        /// <value>The date stored in the cache.</value>
        private T Cache
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the timestamp when the cache was last
        /// updated.
        /// </summary>
        /// <value>The timestamp when the cache was last
        /// updated.</value>
        private DateTime LastUpdatedAt
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the lock handle used to lock the 
        /// cache during access.
        /// </summary>
        /// <value>The lock handle.</value>
        private object LockHandle
        {
            get;
            set;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// This method is used to actually pull the data out of the underlying
        /// cache.  If the cache has expired, it will be refreshed by calling
        /// the delegate that is in charge of populating it.
        /// </summary>
        /// <returns>the object that is cached</returns>
        public T RetrieveData()
        {
            using ( new LockHolder<object>(LockHandle, TIMEOUT) )
            {
                double millisPassed = DateTime.Now.Subtract(LastUpdatedAt).TotalMilliseconds;
                if ( millisPassed > TimeToLive )
                {
                    // cache is expired - (re)populate the cache
                    Cache = this.InvokeCachePopulater();
                    LastUpdatedAt = DateTime.Now;

                }
            }

            return Cache;
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Invoke the delegate to actually populate the cache.  It is marked
        /// as virtual so that it can be overridden in sub-classes
        /// </summary>
        /// <returns>a collection of strongly-typed objects</returns>
        protected virtual T InvokeCachePopulater()
        {
            return this.CachePopulater();
        }

        #endregion

    }
}